/*
*  linux/kernel/hd.c
*
*  (C) 1991  Linus Torvalds
*/

/*
* This is the low-level hd interrupt support. It traverses the
* request-list, using interrupts to jump between functions. As
* all the functions are called within interrupts, we may not
* sleep. Special care is recommended.
*
*  modified by Drew Eckhardt to check nr of hd's from the CMOS.
*/

#include <linux\config.h>
#include <linux\sched.h>
#include <linux\fs.h>
#include <linux\kernel.h>
#include <linux\hdreg.h>
#include <asm\system.h>
#include <asm\io.h>
#include <asm\segment.h>

#define MAJOR_NR 3
#include "blk.h"

__inline unsigned char CMOS_READ(unsigned short addr)
{
	unsigned char _v;

	outb_p(0x80 | addr, 0x70);
	_v = inb_p(0x71);

	return _v;
}

/* Max read/write errors/sector */
#define MAX_ERRORS	7
#define MAX_HD		2

static void recal_intr(void);

static int recalibrate = 1;
static int reset = 1;

/*
*  This struct defines the HD's and their types.
*/
struct hd_i_struct {
	int head, sect, cyl, wpcom, lzone, ctl;
};
#ifdef HD_TYPE
struct hd_i_struct hd_info[] = { HD_TYPE };
#define NR_HD ((sizeof (hd_info))/(sizeof (struct hd_i_struct)))
#else
struct hd_i_struct hd_info[] = { { 0, 0, 0, 0, 0, 0 }, { 0, 0, 0, 0, 0, 0 } };
static int NR_HD = 0;
#endif

static struct hd_struct {
	long start_sect;
	long nr_sects;
} hd[5 * MAX_HD] = { { 0, 0 }, };

static __inline void port_read(unsigned short port, void *buf, int nr)
{
	__asm mov dx, port
	__asm mov edi, buf
	__asm mov ecx, nr
	__asm cld
	__asm rep insw
}

static __inline void port_write(unsigned short port, void *buf, int nr)
{
	__asm mov dx, port
	__asm mov esi, buf
	__asm mov ecx, nr
	__asm cld
	__asm rep outsw
}

extern void hd_interrupt(void);
extern void rd_load(void);

/* This may be used only once, enforced by 'static int callable' */
int sys_setup(char * BIOS)
{
	static int callable = 1;
	int i, drive;
	unsigned char cmos_disks;
	struct partition *p;
	struct buffer_head *bh;

	if (!callable)
		return -1;
	callable = 0;
#ifndef HD_TYPE
	for (drive = 0; drive < 2; drive++) {
		hd_info[drive].cyl = *(unsigned short *)BIOS;
		hd_info[drive].head = *(unsigned char *)(2 + BIOS);
		hd_info[drive].wpcom = *(unsigned short *)(5 + BIOS);
		hd_info[drive].ctl = *(unsigned char *)(8 + BIOS);
		hd_info[drive].lzone = *(unsigned short *)(12 + BIOS);
		hd_info[drive].sect = *(unsigned char *)(14 + BIOS);
		BIOS += 16;
	}
	if (hd_info[1].cyl)
		NR_HD = 2;
	else
		NR_HD = 1;
#endif
	for (i = 0; i < NR_HD; i++) {
		hd[i * 5].start_sect = 0;
		hd[i * 5].nr_sects = hd_info[i].head*
			hd_info[i].sect*hd_info[i].cyl;
	}

	/*
	We querry CMOS about hard disks : it could be that
	we have a SCSI/ESDI/etc controller that is BIOS
	compatable with ST-506, and thus showing up in our
	BIOS table, but not register compatable, and therefore
	not present in CMOS.

	Furthurmore, we will assume that our ST-506 drives
	<if any> are the primary drives in the system, and
	the ones reflected as drive 1 or 2.

	The first drive is stored in the high nibble of CMOS
	byte 0x12, the second in the low nibble.  This will be
	either a 4 bit drive type or 0xf indicating use byte 0x19
	for an 8 bit type, drive 1, 0x1a for drive 2 in CMOS.

	Needless to say, a non-zero value means we have
	an AT controller hard disk for that drive.
	*/

	if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)
		if (cmos_disks & 0x0f)
			NR_HD = 2;
		else
			NR_HD = 1;
	else
		NR_HD = 0;
	for (i = NR_HD; i < 2; i++) {
		hd[i * 5].start_sect = 0;
		hd[i * 5].nr_sects = 0;
	}
	for (drive = 0; drive < NR_HD; drive++) {
		if (!(bh = bread(0x300 + drive * 5, 0))) {
			printk("Unable to read partition table of drive %d\n\r",
				drive);
			panic("");
		}
		if (bh->b_data[510] != 0x55 || (unsigned char)
			bh->b_data[511] != 0xAA) {
			printk("Bad partition table on drive %d\n\r", drive);
			panic("");
		}
		p = (struct partition*)(0x1BE + (char*)bh->b_data);
		for (i = 1; i<5; i++, p++) {
			hd[i + 5 * drive].start_sect = p->start_sect;
			hd[i + 5 * drive].nr_sects = p->nr_sects;
		}
		brelse(bh);
	}
	if (NR_HD)
		printk("Partition table%s ok.\n\r", (NR_HD>1) ? "s" : "");
	rd_load();
	mount_root();
	return (0);
}

static int controller_ready(void)
{
	int retries = 10000;

	while (--retries && (inb_p(HD_STATUS) & 0xc0) != 0x40);
	return (retries);
}

static int win_result(void)
{
	int i = inb_p(HD_STATUS);

	if ((i & (BUSY_STAT | READY_STAT | WRERR_STAT | SEEK_STAT | ERR_STAT))
		== (READY_STAT | SEEK_STAT))
		return(0); /* ok */
	if (i & 1) i = inb(HD_ERROR);
	return (1);
}

static void hd_out(unsigned int drive, unsigned int nsect, unsigned int sect,
	unsigned int head, unsigned int cyl, unsigned int cmd,
	void(*intr_addr)(void))
{
	register int port;

	if (drive > 1 || head > 15)
		panic("Trying to write bad sector");
	if (!controller_ready())
		panic("HD controller not ready");
	do_hd = intr_addr;
	outb_p(hd_info[drive].ctl, HD_CMD);
	port = HD_DATA;
	outb_p(hd_info[drive].wpcom >> 2, ++port);
	outb_p(nsect, ++port);
	outb_p(sect, ++port);
	outb_p(cyl, ++port);
	outb_p(cyl >> 8, ++port);
	outb_p(0xA0 | (drive << 4) | head, ++port);
	outb(cmd, ++port);
}

static int drive_busy(void)
{
	unsigned int i;

	for (i = 0; i < 10000; i++)
		if (READY_STAT == (inb_p(HD_STATUS) & (BUSY_STAT | READY_STAT)))
			break;
	i = inb(HD_STATUS);
	i &= BUSY_STAT | READY_STAT | SEEK_STAT;
	if (i == (READY_STAT | SEEK_STAT))
		return(0);
	printk("HD controller times out\n\r");
	return(1);
}

static void reset_controller(void)
{
	int	i;

	outb(4, HD_CMD);
	for (i = 0; i < 100; i++) nop();
	outb(hd_info[0].ctl & 0x0f, HD_CMD);
	if (drive_busy())
		printk("HD-controller still busy\n\r");
	if ((i = inb(HD_ERROR)) != 1)
		printk("HD-controller reset failed: %02x\n\r", i);
}

static void reset_hd(int nr)
{
	reset_controller();
	hd_out(nr, hd_info[nr].sect, hd_info[nr].sect, hd_info[nr].head - 1,
		hd_info[nr].cyl, WIN_SPECIFY, &recal_intr);
}

void unexpected_hd_interrupt(void)
{
	printk("Unexpected HD interrupt\n\r");
}

static void bad_rw_intr(void)
{
	if (++CURRENT->errors >= MAX_ERRORS)
		end_request(0);
	if (CURRENT->errors > MAX_ERRORS / 2)
		reset = 1;
}

static void read_intr(void)
{
	if (win_result()) {
		bad_rw_intr();
		do_hd_request();
		return;
	}
	port_read(HD_DATA, CURRENT->buffer, 256);
	CURRENT->errors = 0;
	CURRENT->buffer += 512;
	CURRENT->sector++;
	if (--CURRENT->nr_sectors) {
		do_hd = &read_intr;
		return;
	}
	end_request(1);
	do_hd_request();
}

static void write_intr(void)
{
	if (win_result()) {
		bad_rw_intr();
		do_hd_request();
		return;
	}
	if (--CURRENT->nr_sectors) {
		CURRENT->sector++;
		CURRENT->buffer += 512;
		do_hd = &write_intr;
		port_write(HD_DATA, CURRENT->buffer, 256);
		return;
	}
	end_request(1);
	do_hd_request();
}

static void recal_intr(void)
{
	if (win_result())
		bad_rw_intr();
	do_hd_request();
}

void do_hd_request(void)
{
	int i, r;
	unsigned int block, dev;
	unsigned int sec, head, cyl;
	unsigned int nsect;

	INIT_REQUEST;
	dev = MINOR(CURRENT->dev);
	block = CURRENT->sector;
	if (dev >= 5 * (unsigned int)NR_HD || block + 2 > (unsigned int)hd[dev].nr_sects) {
		end_request(0);
		goto repeat;
	}
	block += hd[dev].start_sect;
	dev /= 5;
	sec = block % hd_info[dev].sect;
	block /= hd_info[dev].sect;
	head = block % hd_info[dev].head;
	cyl = block / hd_info[dev].head;
	sec++;
	nsect = CURRENT->nr_sectors;
	if (reset) {
		reset = 0;
		recalibrate = 1;
		reset_hd(CURRENT_DEV);
		return;
	}
	if (recalibrate) {
		recalibrate = 0;
		hd_out(dev, hd_info[CURRENT_DEV].sect, 0, 0, 0,
			WIN_RESTORE, &recal_intr);
		return;
	}
	if (CURRENT->cmd == WRITE) {
		hd_out(dev, nsect, sec, head, cyl, WIN_WRITE, &write_intr);
		for (i = 0; i < 3000 && !(r = inb_p(HD_STATUS)&DRQ_STAT); i++)
			/* nothing */;
		if (!r) {
			bad_rw_intr();
			goto repeat;
		}
		port_write(HD_DATA, CURRENT->buffer, 256);
	}
	else if (CURRENT->cmd == READ) {
		hd_out(dev, nsect, sec, head, cyl, WIN_READ, &read_intr);
	}
	else
		panic("unknown hd-command");
}

void hd_init(void)
{
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	set_intr_gate(0x2E, &hd_interrupt);
	outb_p(inb_p(0x21) & 0xfb, 0x21);
	outb(inb_p(0xA1) & 0xbf, 0xA1);
}
